Skip to content

feat(trip): add Trip.com international adapter#2158

Merged
jackwener merged 19 commits into
jackwener:mainfrom
Benjamin-eecs:feat/trip-adapter
Jul 20, 2026
Merged

feat(trip): add Trip.com international adapter#2158
jackwener merged 19 commits into
jackwener:mainfrom
Benjamin-eecs:feat/trip-adapter

Conversation

@Benjamin-eecs

@Benjamin-eecs Benjamin-eecs commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a new trip adapter for Trip.com, the English-facing international sibling of the existing ctrip adapter (same company, Trip.com Group). It covers Trip.com's travel verticals end to end in English / USD, reading rows from stable data-testid / class anchors or SSR, dropping incomplete rows rather than surfacing blanks, and raising AuthRequiredError on a verification gate. Most commands are browser-mode plus Strategy.COOKIE like the ctrip peers; search and package resolve through public unsigned endpoints and need no browser session.

Commands in this PR (each live-verified, output below):

  • Discovery: search (public destination / airport suggest; resolves the ids the other commands take)
  • Flights: flight (one-way), flight-round (round-trip)
  • Hotels: hotel-search (city listing), hotel (single-hotel detail)
  • Attractions & experiences: attraction (tickets, tours, experiences)
  • Trains: train (route timetable)
  • Cars: car (car-rental listing by city)
  • Airport transfers: transfer (city + airport vehicle listing)
  • Tours: tour (private / group tour packages)
  • Flight+hotel packages: package (public; package flight options at the bundle rate)
  • Deals: deals (live-promotions hub)

On #2157's esim: Trip.com sells eSIM / SIM data plans as things-to-do products, so they already surface through attraction (e.g. opencli trip attraction eSIM returns them, and the ids match the eSIM page's own buy buttons). A separate esim command would only hardcode that keyword against the same endpoint and extractor, so it is covered by attraction rather than duplicated.

Related issue: Closes #2157.

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🌐 New site adapter
  • 📝 Documentation
  • ♻️ Refactor
  • 🔧 CI / build / tooling

Checklist

  • I ran the checks relevant to this PR
  • I updated tests or docs if needed
  • I included output or screenshots when useful

Documentation (if adding/modifying an adapter)

  • Added doc page under docs/adapters/ (if new adapter)
  • Updated docs/adapters/index.md table (if new adapter)
  • Updated sidebar in docs/.vitepress/config.mts (if new adapter)
  • Updated README.md / README.zh-CN.md when command discoverability changed
  • Used positional args for the command's primary subject unless a named flag is clearly better
  • Normalized expected adapter failures to CliError subclasses instead of raw Error

Screenshots / Output

All twelve commands are live-verified against a Trip.com session (English / USD), no verification gate.

Discovery: search

$ opencli trip search Tokyo --limit 3 -f json
[
  {"rank":1,"name":"Tokyo","type":"city","cityId":228,"airportCode":null,"province":null,"country":"Japan"},
  {"rank":2,"name":"Narita International Airport","type":"airport","cityId":228,"airportCode":"NRT","province":null,"country":"Japan"},
  {"rank":3,"name":"Haneda Airport","type":"airport","cityId":228,"airportCode":"HND","province":null,"country":"Japan"}
]

search is a public unsigned POST to Trip.com's POI-suggest endpoint (the same lookup the flight / hotel boxes use, no browser session). Each city keeps its own row and its nearby airports follow, so one call surfaces both the cityId the id-based commands take and the airportCode the flight / transfer commands take.

Flights: flight (one-way)

$ opencli trip flight LON NYC --date 2026-08-15 --limit 2 -f json
[
  { "rank": 1, "airline": "Norse Atlantic Airways", "departureTime": "1:05 PM", "departureAirport": "LGW", "arrivalTime": "3:55 PM", "arrivalAirport": "JFK", "duration": "7h 50m", "stops": "Nonstop", "price": 662, "currency": "USD" },
  { "rank": 2, "airline": "Jetblue Airways", "departureTime": "8:15 AM", "departureAirport": "LHR", "arrivalTime": "11:30 AM", "arrivalAirport": "JFK", "duration": "8h 15m", "stops": "Nonstop", "price": 837, "currency": "USD" }
]

Flights: flight-round (round-trip)

$ opencli trip flight-round LON NYC --depart 2026-08-15 --return 2026-08-22 --limit 2 -f json
[
  { "rank": 1, "airline": "British Airways", "departureTime": "6:05 PM", "departureAirport": "LHR", "arrivalTime": "9:05 PM", "arrivalAirport": "JFK", "duration": "8h", "stops": "Nonstop", "price": 758, "currency": "USD" },
  { "rank": 2, "airline": "American Airlines", "departureTime": "6:05 PM", "departureAirport": "LHR", "arrivalTime": "9:05 PM", "arrivalAirport": "JFK", "duration": "8h", "stops": "Nonstop", "price": 767, "currency": "USD" }
]

flight-round returns the outbound leg priced for the round trip, reusing the same .result-item extractor against a triptype=rt search URL.

Hotels: hotel-search (city listing)

$ opencli trip hotel-search 338 --checkin 2026-08-15 --checkout 2026-08-16 --limit 2 -f json
[
  { "rank": 1, "name": "LSE Rosebery Hall", "score": 8.3, "reviewLabel": "Very good", "reviews": 159, "location": "Near The British Museum, King's Cross Station", "room": "Standard Twin Room, 2 Twin Beds, Shared Bathroom", "price": 116, "currency": "USD" },
  { "rank": 2, "name": "Strand Palace", "score": 9.1, "reviewLabel": "Great", "reviews": 513, "location": "Theatreland, Near The British Museum", "room": "Superior Queen Room", "price": 363, "currency": "USD" }
]

Hotels: hotel (single-hotel detail)

$ opencli trip hotel 715233 -f json
[
  { "hotelId": "715233", "name": "LSE Rosebery Hall", "enName": null, "star": 2, "score": 8.3, "scoreLabel": "Very good", "reviewCount": 159, "ratingBreakdown": "Cleanliness 8.7 / Amenities 7.7 / Location 8.5 / Service 8.3", "facilities": "Luggage storage / Wi-Fi in public areas / Billiards room / Restaurant", "checkInOut": "Check-in: After 15:00 / Check-out: Before 10:30 / Front desk hours: 24/7", "cityName": "London", "address": "90 Rosebery Ave, Islington, London, EC1R 4TY, United Kingdom", "lat": 51.527561, "lon": -0.107065 }
]

hotel reads the SSR __NEXT_DATA__.props.pageProps.hotelDetailResponse, the same shape the mainland ctrip hotel detail uses; room-level nightly prices load via a post-SSR XHR and are out of scope, since hotel-search already reports a representative nightly price.

Attractions & experiences: attraction

$ opencli trip attraction Tokyo --limit 2 -f json
[
  { "rank": 1, "name": "[Official Ticket] Tokyo Metro 24/48/72 Hour Pass ...", "rating": 4.8, "reviews": 4900, "booked": 109500, "price": 5.54, "currency": "USD", "url": "https://us.trip.com/things-to-do/detail/24465457/" },
  { "rank": 2, "name": "Mount Fuji Day Trip from Tokyo ...", "rating": 4.9, "reviews": 3000, "booked": 40100, "price": 36.29, "currency": "USD", "url": "https://us.trip.com/things-to-do/detail/60225529/" }
]

The things-to-do products load into hashed CSS-module cards, so rows anchor on each card's stable things-to-do/detail/<id> link (name is its text, and it gives a real per-row url) and read rating / reviews / booked / price from the card text by data-format pattern; the promo $N off tag is excluded. This covers Trip.com's combined "Attractions & Tours" (tickets, tours, experiences), and, since Trip.com files eSIM / SIM data plans as things-to-do products, #2157's eSIM vertical too (opencli trip attraction eSIM).

Trains: train

$ opencli trip train London Manchester --country uk --limit 3 -f json
[
  { "rank": 1, "departureTime": "05:27", "fromStation": "London St. Pancras International", "arrivalTime": "09:05", "toStation": "Manchester Piccadilly", "duration": "3h 38m", "changes": 1 },
  { "rank": 2, "departureTime": "05:59", "fromStation": "London St. Pancras International", "arrivalTime": "09:32", "toStation": "Manchester Piccadilly", "duration": "3h 33m", "changes": 1 },
  { "rank": 3, "departureTime": "06:08", "fromStation": "London Euston", "arrivalTime": "08:25", "toStation": "Manchester Piccadilly", "duration": "2h 17m", "changes": 0 }
]

Trip.com files train routes as per-country SEO timetable pages, so --country is required; the page lists journeys (departure / arrival times, stations, duration, changes) by stable class fields. Per-journey fares sit behind the booking step and are out of scope here.

Cars: car

$ opencli trip car 313 --limit 3 -f json
[
  { "rank": 1, "category": "Mid-sized car", "vehicle": "Toyota Camry or Similar", "seats": 5, "price": 50, "currency": "USD", "url": "https://www.trip.com/carhire/to-city-1/city-313/" },
  { "rank": 2, "category": "Small SUV", "vehicle": "Hyundai Venue or Similar", "seats": 5, "price": 96, "currency": "USD", "url": "https://www.trip.com/carhire/to-city-1/city-313/" },
  { "rank": 3, "category": "Compact car", "vehicle": "Toyota Corolla or Similar", "seats": 5, "price": 47, "currency": "USD", "url": "https://www.trip.com/carhire/to-city-1/city-313/" }
]

Trip.com files car-rental listings under an SEO path whose text slugs are cosmetic, so only the numeric carhire city id routes the page (313 is San Francisco, discovered via the carhire search box, the same discovery flow hotel-search uses). Rows come from .card-item cards read by stable class fields; the daily price is the site's near-term representative rate, and a dated pickup / drop-off quote sits behind the booking step.

Airport transfers: transfer

$ opencli trip transfer Bangkok DMK --limit 4 -f json
[
  { "rank": 1, "type": "Standard Car", "passengers": 4, "luggage": 1, "price": 15.73, "currency": "USD", "url": "https://www.trip.com/airport-transfers/bangkok/airport-dmk/" },
  { "rank": 2, "type": "Medium Car", "passengers": 4, "luggage": 1, "price": 21.19, "currency": "USD", "url": "https://www.trip.com/airport-transfers/bangkok/airport-dmk/" },
  { "rank": 3, "type": "Minivan", "passengers": 4, "luggage": 2, "price": 21.45, "currency": "USD", "url": "https://www.trip.com/airport-transfers/bangkok/airport-dmk/" },
  { "rank": 4, "type": "Minibus", "passengers": 9, "luggage": 3, "price": 30.81, "currency": "USD", "url": "https://www.trip.com/airport-transfers/bangkok/airport-dmk/" }
]

Trip.com files airport transfers under an SEO path keyed on the city slug plus the airport IATA code (airport-transfers/<city>/airport-<iata>/). A city that does not match the airport bounces to the transfer landing, so the command checks the landed path and raises CommandExecutionError rather than returning the generic landing list (verified: trip transfer Paris DMK errors instead of returning Bangkok's list). Rows come from .vehicle-card cards read by stable class fields; the $N off promo sits in a separate discount tag and is excluded.

Tours: tour

$ opencli trip tour Kyoto --limit 2 -f json
[
  {"rank":1,"name":"2D1N · Private Tours · Japan Osaka + Kyoto + Nara Kansai Three-City Travel Route","type":"Private Tours","rating":null,"reviews":null,"price":88,"currency":"USD","url":"https://us.trip.com/package-tours/detail/70457661?city=219&lineproductid=70457661&locale=en-US&curr=USD"},
  {"rank":2,"name":"6D5N · Private Tours · Japan + Osaka + Kyoto + Kobe + Wakayama City ...","type":"Private Tours","rating":null,"reviews":null,"price":1499,"currency":"USD","url":"https://us.trip.com/package-tours/detail/68370533?city=219&lineproductid=68370533&locale=en-US&curr=USD"}
]

Trip.com serves tour results through a signed POST that only fires on a search submit, so rather than replaying the signature the command opens the results page and re-submits the keyword in the page's own search box, letting the page issue its own signed request while a fetch hook captures the products response. --type private / group selects the product line; per-departure pricing sits behind the booking step, so price is the starting per-person estimate.

Flight+hotel packages: package

$ opencli trip package Seoul Tokyo --depart 2026-08-05 --return 2026-08-08 --limit 2 -f json
[
  {"rank":1,"airline":"T'Way Air","flightNo":"TW249","from":"ICN","to":"NRT","departure":"2026-08-05 20:10:00","arrival":"2026-08-05 22:40:00","stops":0,"price":52.3,"currency":"USD"}
]

Trip.com prices flight+hotel packages through the flight-selection step of the booking flow: a public unsigned POST keyed on the metro city codes plus the destination hotel city id returns the outbound flight options priced at the bundle rate. So package resolves both keywords to their city code + id through the same POI search search uses, then lists those flights with no browser session. The specific hotel is chosen in a later booking step, so per-row hotel detail and the return leg (which rides on the hotel checkout date) are out of scope here.

Deals: deals

$ opencli trip deals --limit 3 -f json
[
  {"rank":1,"title":"Explore Asia","offer":"Travel Deals up to 30% OFF","discount":"30%","url":"https://us.trip.com/sale/w/19595/exploreasia.html"},
  {"rank":2,"title":"Explore North America's Host Cities","offer":"Explore North America's 2026 World Cup host cities with Trip.com and find exclusive deals on flights, hotels, attractions, and travel guides across the US, Canada, and Mexico.","discount":null,"url":"https://us.trip.com/sale/w/34049/2026naworldcup.html"},
  {"rank":3,"title":"Go China","offer":"Exclusive Summer Deals","discount":null,"url":"https://us.trip.com/sale/w/19280/gochina.html"}
]

deals reads Trip.com's "Today's Top Deals" hub, one row per running campaign (title, offer line, discount parsed from the text, and the canonical campaign URL with the per-session promo tracking stripped). Tiles without a title or offer are dropped, and rows are deduped by campaign URL.

Notes for review:

  • This is a separate adapter from ctrip, not a flag on it: different domain (trip.com), language (English), currency (USD), and DOM. This matches how the repo keeps region variants apart (e.g. rednote vs xiaohongshu).
  • Each command anchors on a stable handle: data-testid cards (flight / flight-round), stable class fields (hotel-search, car, transfer, deals), the SSR detail response (hotel), the per-product detail link (attraction), timetable class cells (train), the page's own captured signed request (tour), or a public unsigned JSON endpoint (search, package). Incomplete rows are dropped, never surfaced with sentinels; price and numeric fields stay null when non-numeric.
  • A verification gate raises AuthRequiredError. Rendered-but-unparseable drift raises CommandExecutionError (render timeout, malformed extraction, a transfer landing-page bounce, a package response whose groups carry no parseable itinerary, a deals hub that renders but parses no tiles, or a tour capture whose products carry no name), keeping it distinct from a genuinely empty result, which raises EmptyResultError (e.g. tour reads the page's 0 routes found). --limit (1-50) is validated up front with no silent clamp.

clis/trip/trip.test.js passes 110 / 110 locally (arg validation, the verification / timeout / empty / drift ladder per command, the public-endpoint fetch / invalid-JSON / drift paths, and JSDOM / SSR extraction). npm run check:typed-error-lint and npm run check:silent-column-drop both report new=0; scripts/check-doc-coverage.sh --strict passes.

Trip.com is the English-facing sibling of the ctrip adapter. trip flight
searches worldwide one-way flights, browser-mode + cookie like ctrip flight.
Results are read from .result-item cards by stable data-testid anchors rather
than positional innerText; incomplete cards are dropped, not sentinel-filled.

Closes jackwener#2157
Single-hotel profile from the detail-page SSR (same shape ctrip hotel uses); also documents the existing hotel-search command.
Reuses the shared .result-item flight extractor against a triptype=rt search URL.
@Benjamin-eecs Benjamin-eecs changed the title feat(trip): add Trip.com international flight search adapter feat(trip): add Trip.com international adapter (flights + hotels) Jul 19, 2026
The 1-50 limit parser is shared by hotel-search and both flight commands, so a neutral name reads truer than the flight-specific one.
@Benjamin-eecs
Benjamin-eecs marked this pull request as ready for review July 19, 2026 23:04
Copilot AI review requested due to automatic review settings July 19, 2026 23:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Anchors on each things-to-do card's stable detail link (name + per-row url) and reads rating/reviews/booked/price by data-format pattern, since the cards use hashed CSS-module classes.
Reads the per-country SEO route timetable (departure/arrival times, stations, duration, changes) by stable class fields; per-journey fares sit behind the booking step.
@Benjamin-eecs
Benjamin-eecs marked this pull request as draft July 20, 2026 02:26
@Benjamin-eecs Benjamin-eecs changed the title feat(trip): add Trip.com international adapter (flights + hotels) feat(trip): add Trip.com international adapter Jul 20, 2026
@Benjamin-eecs
Benjamin-eecs marked this pull request as ready for review July 20, 2026 16:55
@jackwener
jackwener merged commit 0124c4e into jackwener:main Jul 20, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: add Trip.com (international) adapter

3 participants